﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Lucene.Linq.Mapping;
using Lucene.Linq.Utility;
using Lucene.Net.Search;

namespace Lucene.Linq.Expressions
{
    /// <summary>
    /// TODO: 
    /// add paging support
    /// add better counting
    /// add better
    /// explore linq provider project as a source
    /// </summary>
    internal class QueryProvider : IQueryProvider
    {
        #region Fields/Properties

        private readonly IndexContext _context;
        private readonly IndexDetails _details;

        private StringBuilder _queryString;


        internal StringBuilder QueryString
        {
            get { return _queryString ?? (_queryString = new StringBuilder()); }
        }

        #endregion

        #region Constructors

        internal QueryProvider(IndexContext context, IndexDetails details)
        {
            if (context == null) throw new ArgumentNullException("context");
            if (details == null) throw new ArgumentNullException("details");
            _context = context;
            _details = details;
        }

        #endregion

        #region IQueryProvider Members

        IQueryable<S> IQueryProvider.CreateQuery<S>(Expression expression)
        {
            return new Query<S>(_context, _details, expression);
        }

        IQueryable IQueryProvider.CreateQuery(Expression expression)
        {
            Type elementType = TypeSystem.GetElementType(expression.Type);
            try
            {
                return
                    (IQueryable)
                    Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType),
                                             new object[] { this, expression });
            }
            catch (TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
        }

        S IQueryProvider.Execute<S>(Expression expression)
        {
            return (S)Execute(expression);
        }

        #endregion

        #region Instance Methods

        public object Execute(Expression expression)
        {
            return Execute(Translate(expression));
        }

        private object Execute(TranslateResult result)
        {
            // Compile the Projector
            Delegate projector = result.Projector.Compile();

            using (_context.EnterReadLock())
            {
                Hits hits = _context.Searcher.Search(result.Query, result.Sort);

                //
                // check for count method
                //
                if (result.GetCount)
                {
                    if (result.Paging == null)
                    {
                        throw new InvalidOperationException("Paging should be set");
                    }

                    if (result.Paging.TakeAmount > 0)
                        return result.Paging.TakeAmount;

                    return hits.Length() - result.Paging.StartIndex;
                }

                Type elementType = TypeSystem.GetElementType(result.Projector.Body.Type);

                //
                // check for single element return
                //

                bool shouldReturnOneItem = result.ElementAt.HasValue || result.GetLast;
                int elementAt = result.ElementAt ?? (hits.Length() - 1);

                // Check for Element At
                if (shouldReturnOneItem)
                {
                    // Create the enumerator
                    var resultEnumerator = Activator.CreateInstance(
                        typeof(ProjectionReader<>).MakeGenericType(elementType),
                        BindingFlags.Instance | BindingFlags.NonPublic,
                        null,
                        new object[]
                            {
                                hits,
                                elementAt,
                                projector,
                                this
                            },
                        null
                                               ) as IEnumerable;

                    // Check for null
                    if (resultEnumerator == null)
                        throw new Exception(
                            "result enumerator as IEnumerable is null for some reason. it really shouldn't be");

                    // get the instance and return it as an object
                    return resultEnumerator.GetEnumerator().Current;
                }

                return Activator.CreateInstance(
                    typeof(ProjectionReader<>).MakeGenericType(elementType),
                    BindingFlags.Instance | BindingFlags.NonPublic,
                    null,
                    new object[]
                        {
                            hits,
                            result.Paging.StartIndex,
                            result.Paging.TakeAmount,
                            projector,
                            this
                        },
                    null
                    );
            }
        }


        private TranslateResult Translate(Expression expression)
        {
            _queryString = new StringBuilder();
            var projection = expression as ProjectionExpression;

            if (projection == null)
            {
                expression = Evaluator.PartialEval(expression);
                projection = (ProjectionExpression)new QueryBinder().Bind(expression);
            }

            int? elementAt;
            var sortFields = new List<SortField>();


            // REVIEW: is this walk methods algorithm flawed?
            // I'm using specific Expression Types if needed, what's the right way to do this?
            // Skip needs to take into account all the skip calls chained together, if this approach is wrong, 
            // how do I accomplish this?
            // UPDATE: I could perform these steps in the during second-exp-tree-visit.. this works though.

            int startIndex = GetFirstArgumentOfMethodCalls<int>(expression, e => e.Method.Name == "Skip").Sum();
            int takeAmount = GetFirstArgumentOfMethodCalls<int>(expression, e => e.Method.Name == "Take").LastOrDefault();
            try
            {
                elementAt = GetFirstArgumentOfMethodCalls<int>(expression, e => e.Method.Name == "ElementAt").Last();
            }
            catch (Exception)
            {
                elementAt = null;
            }


            // Ordering by
            foreach (OrderByInfo orderByInfo in GetOrderByFields(expression))
            {
                // TODO: detect sort field type
                sortFields.Insert(0, new SortField(orderByInfo.Field.SortName, orderByInfo.IsDescending));
            }

            //
            // first and last
            //
            if (CountMethodCallsWithName(expression, "First") > 0)
            {
                elementAt = 0;
            }
            bool getLast = CountMethodCallsWithName(expression, "Last") > 0;

            // translate the expression into the lucene query
            // TODO: support switchable translators

            ISearchQuery query = new ParsingQueryTranslator(QueryString).Translate(projection.Source);

            // build the actual projection expression
            LambdaExpression projector = new ProjectionBuilder().Build(projection.Projector, projection.Source.Alias);

            return new TranslateResult
                       {
                           Query = query.Query,
                           Sort = sortFields.Count == 0 ? null : new Sort(sortFields.ToArray()),
                           // Null or the right sort field
                           Projector = projector,
                           GetCount = projection is CountExpression,
                           Paging = new ResultPaging
                                        {
                                            StartIndex = startIndex,
                                            TakeAmount = takeAmount
                                        },
                           ElementAt = elementAt,
                           GetLast = getLast
                       };
        }

        private IEnumerable<OrderByInfo> GetOrderByFields(Expression exp)
        {
            var orderByMethodNames = new[] { "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending" };
            Expression current = exp;
            while (current.NodeType == ExpressionType.Call)
            {
                var mce = (current as MethodCallExpression);

                if (mce != null)
                {
                    if (orderByMethodNames.Contains(mce.Method.Name))
                    {
                        var quotedWrapper = mce.Arguments[1] as UnaryExpression;
                        if (quotedWrapper != null)
                        {
                            var orderByLambda = quotedWrapper.StripQuotes() as LambdaExpression;
                            if (orderByLambda != null)
                            {
                                PropertyInfo propInfo = orderByLambda.FindPropertyInfoAccess();

                                FieldDetails foundFieldDetails = (from f in _details.Document.Fields
                                                                  where f.Property == propInfo
                                                                  select f).FirstOrDefault();

                                if (foundFieldDetails == null)
                                    throw new NotSupportedException(String.Format(
                                        "{0} is not marked as a lucene field",
                                        propInfo.Name));

                                // extract the field
                                yield return new OrderByInfo(foundFieldDetails, mce.Method.Name.EndsWith("Descending"));
                            }
                        }
                    }
                }

                current = mce.Arguments[0];
            }
        }

        private static IEnumerable<T> GetFirstArgumentOfMethodCalls<T>(Expression exp, Predicate<MethodCallExpression> pred)
        {
            if (exp == null)
            {
                throw new ArgumentNullException("exp");
            }
            if (pred == null)
            {
                throw new ArgumentNullException("pred");
            }

            Expression current = exp;
            while (current.NodeType == ExpressionType.Call)
            {
                var mce = (current as MethodCallExpression);

                if (pred(mce))
                {
                    if (mce.Arguments[1] is ConstantExpression)
                    {
                        yield return (T)(mce.Arguments[1] as ConstantExpression).Value;
                    }
                }
                current = mce.Arguments[0];
            }
        }

        private int CountMethodCallsWithName(Expression exp, string name)
        {
            return CountMethodCalls(exp, me => me.Method.Name == name);
        }

        private int CountMethodCalls(Expression exp, Predicate<MethodCallExpression> pred)
        {
            if (exp == null)
            {
                throw new ArgumentNullException("exp");
            }
            if (pred == null)
            {
                throw new ArgumentNullException("pred");
            }

            Expression current = exp;
            int count = 0;
            while (current.NodeType == ExpressionType.Call)
            {
                var mce = (current as MethodCallExpression);

                if (pred(mce))
                {
                    count++;
                }
                current = mce.Arguments[0];
            }
            return count;
        }

        #endregion
    }
}